#!/usr/bin/env bash

######################################################################
# @author      : alpha (alpha@mascot)
# @created     : Sunday May 02, 2021 22:48:13 CST
#
# @description : create root file system with busybox
######################################################################

#
# see: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_02
#                    | parameter Set and Not Null | parameter Set But Null | parameter Unset
# ${parameter:-word} | substitute parameter       | substitute word        | substitute word
# ${parameter-word}  | substitute parameter       | substitute null        | substitute word
# ${parameter:=word} | substitute parameter       | assign word            | assign word
# ${parameter=word}  | substitute parameter       | substitute null        | assign word
# ${parameter:?word} | substitute parameter       | error, exit            | error, exit
# ${parameter?word}  | substitute parameter       | substitute null        | error, exit
# ${parameter:+word} | substitute word            | substitute null        | substitute null
# ${parameter+word}  | substitute word            | substitute word        | substitute null
#

# default values
DEFAULT_OUTPUT_DIR=./images
DEFAULT_ROOTFS_DIR=./rootfs
DEFAULT_ROOTFS_SIZE=128M
DEFAULT_ROOTFS_TYPE=ext4
DEFAULT_ROOTFS_CPIO=y
DEFAULT_ROOTFS_IMAGE=n
DEFAULT_MKFS_OPT=
DEFAULT_HOSTNAME=mascot.zone

# default option arguments
ARG_OUTPUT_DIR=${DEFAULT_OUTPUT_DIR}
ARG_ROOTFS_DIR=${DEFAULT_ROOTFS_DIR}
ARG_ROOTFS_SIZE=${DEFAULT_ROOTFS_SIZE}
ARG_BUSYBOX_DIR=
ARG_LIB_DIR=
ARG_ROOTFS_CPIO=${DEFAULT_ROOTFS_CPIO}
ARG_ROOTFS_IMAGE=${DEFAULT_ROOTFS_IMAGE}
ARG_EXTRA=

function usage()
{
	echo -e "A tool to create root file system with busybox."
	echo -e
	echo -e "usage: $0 [OPTIONS]"
	echo -e
	echo -e "OPTIONS:"
	echo -e "\t-h, print this usage and exit"
	echo -e "\t-s <busybox install directory>"
	echo -e "\t-o <output directory>, default: ${DEFAULT_OUTPUT_DIR}"
	echo -e "\t-d <file system root directory>, default: ${DEFAULT_ROOTFS_DIR}"
	echo -e "\t-lib <compiler lib directory>, for dynamically linked programs"
	echo -e "\t-cpio [Y/n], create a cpio file or not, default: ${DEFAULT_ROOTFS_CPIO}"
	echo -e "\t-image [Y/n], create a image file or not, default: ${DEFAULT_ROOTFS_IMAGE}"
	echo -e "\t-size [file system size], default: ${DEFAULT_ROOTFS_SIZE}"
	echo -e "\t-- [optional extra arguments pass to mkfs]"
	echo -e
	echo -e "EXAMPLES:"
	echo -e "\t$0 -s ./busybox-1.32.1/_install"
	echo -e
}

# parse arguments
while [[ -n "$1" ]]; do
	# if $1 is last argument, set arg=--, otherwise set arg=$2
	arg=${2:---}
	case "$1" in
	"-s")
		if [[ $arg == -* ]]; then
			echo -e "missing option $1 argument\n"
			usage
			exit -1
		else
			ARG_BUSYBOX_DIR=$arg
			shift 2
		fi
		;;
	"-o")
		if [[ $arg == -* ]]; then
			echo -e "missing option $1 argument\n"
			usage
			exit -1
		else
			ARG_OUTPUT_DIR=$arg
			shift 2
		fi
		;;
	"-d")
		if [[ $arg == -* ]]; then
			echo -e "missing option $1 argument\n"
			usage
			exit -1
		else
			ARG_ROOTFS_DIR=$arg
			shift 2
		fi
		;;
	"-size")
		if [[ $arg == -* ]]; then
			# base on the device size, man mkfs for detail
			ARG_ROOTFS_SIZE=
			shift 1
		else
			ARG_ROOTFS_SIZE=$arg
			shift 2
		fi
		;;
	"-lib")
		if [[ $arg == -* ]]; then
			echo -e "missing option $1 argument\n"
			usage
			exit -1
		else
			ARG_LIB_DIR=$arg
			shift 2
		fi
		;;
	"-cpio")
		if [[ $arg == -* ]]; then
			ARG_ROOTFS_CPIO=y
			shift 1
		else
			ARG_ROOTFS_CPIO=$arg
			shift 2
		fi
		;;
	"-image")
		if [[ $arg == -* ]]; then
			ARG_ROOTFS_IMAGE=y
			shift 1
		else
			ARG_ROOTFS_IMAGE=$arg
			shift 2
		fi
		;;
	"--")
		shift
		ARG_EXTRA="$@"
		break
		;;
	"-h")
		usage
		exit 0
		;;
	*)
		echo -e "unknown option: $1\n"
		usage
		exit -1
		;;
	esac
done

# remove all suffix '/'
ARG_OUTPUT_DIR=$(echo "${ARG_OUTPUT_DIR}" | sed 's/\/\+$//')
if [[ -z "${ARG_OUTPUT_DIR:+x}" ]]; then
	echo -e "missing output directory\n"
	usage
	exit -1
fi

# remove all suffix '/'
ARG_ROOTFS_DIR=$(echo "${ARG_ROOTFS_DIR}" | sed 's/\/\+$//')
if [[ -z "${ARG_ROOTFS_DIR:+x}" ]]; then
	echo -e "missing file system root directory\n"
	usage
	exit -1
fi

# remove all suffix '/'
ARG_BUSYBOX_DIR=$(echo "${ARG_BUSYBOX_DIR}" | sed 's/\/\+$//')
if [[ -n "${ARG_BUSYBOX_DIR}" ]]; then
	REAL_BUSYBOX_DIR=$(realpath -e ${ARG_BUSYBOX_DIR})
fi

# remove all suffix '/'
ARG_LIB_DIR=$(echo "${ARG_LIB_DIR}" | sed 's/\/\+$//')
if [[ -n "${ARG_LIB_DIR}" ]]; then
	REAL_LIB_DIR=$(realpath -e ${ARG_LIB_DIR})
fi

echo -e "start to make root file system ..."

#########################################################

# create file system root directory
MAKE_ROOTFS_DIR=y
if [[ ${ARG_ROOTFS_DIR} == /* ]]; then
	REAL_ROOTFS_DIR=$(realpath -m ${ARG_ROOTFS_DIR})
else
	REAL_ROOTFS_DIR="$(realpath -m ${ARG_OUTPUT_DIR:-.}/${ARG_ROOTFS_DIR})"
fi
echo -e "\ncreate file system root directory '${REAL_ROOTFS_DIR}' ..."
if [[ -d "${REAL_ROOTFS_DIR}" ]]; then
	read -p "'${REAL_ROOTFS_DIR}' exist, overwrite it? (y/N):" answer
	if [[ "${answer}" != "y" && "${answer}" != "Y" ]]; then
		MAKE_ROOTFS_DIR=n
	fi
fi
if [[ "${MAKE_ROOTFS_DIR:-}" == "y" ]]; then
	mkdir -p ${REAL_ROOTFS_DIR}
	if [[ $? -ne 0 ]]; then
		echo "create '${REAL_ROOTFS_DIR}' failed, abort"
		exit -1
	fi

	pushd . > /dev/null

	echo -e "\nenter '${REAL_ROOTFS_DIR}'"
	cd ${REAL_ROOTFS_DIR}

	# create file system skeleton
	# NOTE: bin, sbin, usr will be created while copying busybox files
	echo -e "\ncreate file system skeleton ..."
	mkdir dev etc etc/init.d home media mnt opt proc root run sys tmp var

	# copy busybox files
	if [[ -n "${REAL_BUSYBOX_DIR:-}" ]]; then
		echo -e "\ncopy busybox files ..."
		cp -r ${REAL_BUSYBOX_DIR}/* ./
	fi

	# create init file for ramdisk, just link to ./sbin/init
	echo -e "\nmake links between 'sbin/init' and './init'"
	ln -s sbin/init ./init

	# copy lib files
	if [[ -n "${REAL_LIB_DIR:-}" ]]; then
		echo -e "\ncopy lib files ..."
		mkdir lib
		cp -pr ${REAL_LIB_DIR}/*.so* ./lib
		ln -s lib lib64
	fi

	CFG_FILE=./etc/fstab
	echo -e "\ncreate file '${CFG_FILE}':"
	echo '#device mount-point type option dump fsck' | tee ${CFG_FILE}
	echo 'proc  /proc proc  defaults 0 0' | tee -a ${CFG_FILE}
	echo 'sysfs /sys  sysfs defaults 0 0' | tee -a ${CFG_FILE}
	echo 'tmpfs /tmp  tmpfs defaults 0 0' | tee -a ${CFG_FILE}
	echo 'tmpfs /dev  tmpfs defaults 0 0' | tee -a ${CFG_FILE}

	CFG_FILE=./etc/init.d/rcS
	echo -e "\ncreate file '${CFG_FILE}':"
	echo '#!/bin/sh' | tee ${CFG_FILE}
	echo '' | tee -a ${CFG_FILE}
	echo '# Managed device files, see Busybox docs/mdev.txt for details' | tee -a ${CFG_FILE}
	echo 'mdev -s' | tee -a ${CFG_FILE}
	echo '' | tee -a ${CFG_FILE}
	echo "echo \"Welcome to ${DEFAULT_HOSTNAME}!\"" | tee -a ${CFG_FILE}
	chmod +x ${CFG_FILE}

	CFG_FILE=./etc/hostname
	echo -e "\ncreate file '${CFG_FILE}':"
	echo "${DEFAULT_HOSTNAME}" | tee ${CFG_FILE}

	CFG_FILE=./etc/inittab
	echo -e "\ncreate file '${CFG_FILE}':"
	echo '# /etc/inittab' | tee ${CFG_FILE}
	echo '#' | tee -a ${CFG_FILE}
	echo '# Copyright (C) 2001 Erik Andersen <andersen@codepoet.org>' | tee -a ${CFG_FILE}
	echo '#' | tee -a ${CFG_FILE}
	echo "# Note: BusyBox init doesn't support runlevels.  The runlevels field is" | tee -a ${CFG_FILE}
	echo '# completely ignored by BusyBox init. If you want runlevels, use' | tee -a ${CFG_FILE}
	echo '# sysvinit.' | tee -a ${CFG_FILE}
	echo '#' | tee -a ${CFG_FILE}
	echo '# Format for each entry: <id>:<runlevels>:<action>:<process>' | tee -a ${CFG_FILE}
	echo '#' | tee -a ${CFG_FILE}
	echo '# id        == tty to run on, or empty for /dev/console' | tee -a ${CFG_FILE}
	echo '# runlevels == ignored' | tee -a ${CFG_FILE}
	echo '# action    == one of sysinit, respawn, askfirst, wait, and once' | tee -a ${CFG_FILE}
	echo '# process   == program to run' | tee -a ${CFG_FILE}
	echo '' | tee -a ${CFG_FILE}
	echo '# Startup the system' | tee -a ${CFG_FILE}
	echo '::sysinit:/bin/mount -t proc proc /proc' | tee -a ${CFG_FILE}
	echo '::sysinit:/bin/mount -o remount,rw /' | tee -a ${CFG_FILE}
	echo '::sysinit:/bin/mkdir -p /dev/pts /dev/shm' | tee -a ${CFG_FILE}
	echo '::sysinit:/bin/mount -a' | tee -a ${CFG_FILE}
	echo '::sysinit:/bin/mkdir -p /run/lock/subsys' | tee -a ${CFG_FILE}
	echo '::sysinit:/bin/ln -sf /proc/self/fd /dev/fd' | tee -a ${CFG_FILE}
	echo '::sysinit:/bin/ln -sf /proc/self/fd/0 /dev/stdin' | tee -a ${CFG_FILE}
	echo '::sysinit:/bin/ln -sf /proc/self/fd/1 /dev/stdout' | tee -a ${CFG_FILE}
	echo '::sysinit:/bin/ln -sf /proc/self/fd/2 /dev/stderr' | tee -a ${CFG_FILE}
	echo '::sysinit:/bin/hostname -F /etc/hostname' | tee -a ${CFG_FILE}
	echo '::sysinit:/etc/init.d/rcS' | tee -a ${CFG_FILE}
	echo '' | tee -a ${CFG_FILE}
	echo '# Login root default' | tee -a ${CFG_FILE}
	echo 'console::sysinit:-/bin/login -f root' | tee -a ${CFG_FILE}
	echo 'console::respawn:-/bin/login -f root' | tee -a ${CFG_FILE}
	echo '#::askfirst:-/bin/login -f root' | tee -a ${CFG_FILE}
	echo '' | tee -a ${CFG_FILE}
	echo '# Stuff to do for the 3-finger salute' | tee -a ${CFG_FILE}
	echo '::ctrlaltdel:/sbin/reboot' | tee -a ${CFG_FILE}
	echo '' | tee -a ${CFG_FILE}
	echo '# Stuff to do before rebooting' | tee -a ${CFG_FILE}
	echo '::shutdown:/bin/umount -a -r' | tee -a ${CFG_FILE}

	CFG_FILE=./etc/passwd
	echo -e "\ncreate file '${CFG_FILE}':"
	echo 'root:x:0:0:root:/root:/bin/sh' | tee ${CFG_FILE}

	CFG_FILE=./etc/group
	echo -e "\ncreate file '${CFG_FILE}':"
	echo 'root:x:0:' | tee ${CFG_FILE}

	CFG_FILE=./etc/profile
	echo -e "\ncreate file '${CFG_FILE}':"
	echo '# /etc/profile: system-wide .profile file for the Bourne shells' | tee ${CFG_FILE}
	echo 'PS1="\e[32;1m[\e[33;1m\u@\H \e[31;1m\w\e[32;1m]\# \e[0m"' | tee -a ${CFG_FILE}
	echo "alias ll='ls -alF'" | tee -a ${CFG_FILE}

	echo -e "\nleave '${REAL_ROOTFS_DIR}'"
	popd >> /dev/null
fi

#########################################################

if [[ "${ARG_ROOTFS_IMAGE:-}" == "y" || "${ARG_ROOTFS_IMAGE:-}" == "Y" ]]; then
	MAKE_ROOTFS_IMAGE=y
	REAL_ROOTFS_IMAGE=$(realpath ${REAL_ROOTFS_DIR}.${DEFAULT_ROOTFS_TYPE})
	echo -e "\ncreate file system file '${REAL_ROOTFS_IMAGE}' ..."
	if [[ -e "${REAL_ROOTFS_IMAGE}" ]]; then
		read -p "'${REAL_ROOTFS_IMAGE}' exist, overwrite it? (y/N):" answer
		if [[ "${answer}" != "y" && "${answer}" != "Y" ]]; then
			MAKE_ROOTFS_IMAGE=n
		fi
	fi
	if [[ "${MAKE_ROOTFS_IMAGE:-}" == "y" ]]; then
		MKFS_OPT="${DEFAULT_MKFS_OPT}"
		MKFS_OPT="${MKFS_OPT:+${MKFS_OPT} }-t ${DEFAULT_ROOTFS_TYPE}"
		MKFS_OPT="${MKFS_OPT} -d ${REAL_ROOTFS_DIR}"
		MKFS_OPT="${MKFS_OPT} ${REAL_ROOTFS_IMAGE}"
		MKFS_OPT="${MKFS_OPT}${ARG_ROOTFS_SIZE:+ ${ARG_ROOTFS_SIZE}}"
		MKFS_OPT="${MKFS_OPT} ${ARG_EXTRA}"
		MKFS_CMD="fakeroot mkfs ${MKFS_OPT}"
		echo "${MKFS_CMD}"
		${MKFS_CMD}
		if [[ $? -ne 0 ]]; then
			echo -e "\ncreate '${REAL_ROOTFS_IMAGE}' failed"
		else
			echo -e "\ncreate '${REAL_ROOTFS_IMAGE}' success"
		fi
	fi
fi

if [[ "${ARG_ROOTFS_CPIO:-}" == "y" || "${ARG_ROOTFS_CPIO:-}" == "Y" ]]; then
	MAKE_ROOTFS_CPIO=y
	MAKE_ROOTFS_CPIO_GZ=n
	REAL_ROOTFS_CPIO=$(realpath ${REAL_ROOTFS_DIR}.cpio)
	REAL_ROOTFS_CPIO_GZ=${REAL_ROOTFS_CPIO}.gz
	echo -e "\ncreate cpio file '${REAL_ROOTFS_CPIO}' ..."
	if [[ -e "${REAL_ROOTFS_CPIO}" ]]; then
		read -p "'${REAL_ROOTFS_CPIO}' exist, overwrite it?(y/N):" answer
		if [[ "${answer}" != "y" && "${answer}" != "Y" ]]; then
			MAKE_ROOTFS_CPIO=n
		fi
	fi
	if [[ "${MAKE_ROOTFS_CPIO:-}" == "y" ]]; then
		CPIO_CMD="cd ${REAL_ROOTFS_DIR} &&"
		CPIO_CMD="${CPIO_CMD} find . -print0 |"
		CPIO_CMD="${CPIO_CMD} cpio -R +0:+0 --null -o --format=newc"
		CPIO_CMD="${CPIO_CMD} -O ${REAL_ROOTFS_CPIO}"
		echo "${CPIO_CMD}"
		eval "${CPIO_CMD}"
		if [[ $? -ne 0 ]]; then
			echo -e "\ncreate '${REAL_ROOTFS_CPIO}' failed"
		else
			echo -e "\ncreate '${REAL_ROOTFS_CPIO}' success"
			MAKE_ROOTFS_CPIO_GZ=y
		fi
	fi
	# create gzip file
	if [[ "${MAKE_ROOTFS_CPIO_GZ:-}" == "y" && -e "${REAL_ROOTFS_CPIO_GZ}" ]]; then
		echo -e "\ncreate gzip file '${REAL_ROOTFS_CPIO_GZ}' ..."
		read -p "'${REAL_ROOTFS_CPIO_GZ}' exist, overwrite it?(y/N):" answer
		if [[ "${answer}" != "y" && "${answer}" != "Y" ]]; then
			MAKE_ROOTFS_CPIO_GZ=n
		fi
	fi
	if [[ "${MAKE_ROOTFS_CPIO_GZ:-}" == "y" ]]; then
		GZIP_CMD="gzip -c ${REAL_ROOTFS_CPIO} > ${REAL_ROOTFS_CPIO_GZ}"
		echo "${GZIP_CMD}"
		eval "${GZIP_CMD}"
		if [[ $? -ne 0 ]]; then
			echo -e "\ncreate '${REAL_ROOTFS_CPIO_GZ}' failed"
		else
			echo -e "\ncreate '${REAL_ROOTFS_CPIO_GZ}' success"
		fi
	fi
fi

echo -e "\nmake root file system finished"
